Skip to content

fix(linear): converge snapshot cache reader/writer keys (TAP-4588)#201

Merged
wtthornton merged 1 commit into
masterfrom
fix-linear-snapshot-cache-key-mismatch
Jul 2, 2026
Merged

fix(linear): converge snapshot cache reader/writer keys (TAP-4588)#201
wtthornton merged 1 commit into
masterfrom
fix-linear-snapshot-cache-key-mismatch

Conversation

@wtthornton

Copy link
Copy Markdown
Owner

Summary

The Linear snapshot payload cache (.tapps-mcp-cache/linear-snapshots/<key>.json) almost never self-hit: the reader (tapps_linear_snapshot_get) and the writer (auto-populate hook + agents' own get/put) computed different keys for the same open-issue slice. The sentinel/gate layer already collapsed aliases (TAP-1374) but the payload key did not.

Fixes TAP-4588.

Root cause + fix

  1. State alias. get(state="open") used the tapps-mcp TTL alias, not a Linear state; real reads use started/unstarted/backlog/"". Added _canonical_state() mapping ""/None/open/open-bucket members to one canonical "open" token; _resolve_cache_key canonicalizes both the filename segment and the hashed state field. Closed buckets (completed/canceled) stay isolated. Mirrored in LINEAR_CACHE_GATE_KEY_PY, LINEAR_CACHE_GATE_POST_LIST_SCRIPT, the PowerShell LINEAR_CACHE_GATE_KEY_PS, and all 3 regenerated committed bash hooks.
  2. limit in the hash. get(limit=150) hashed differently from the writer default limit=50. Dropped limit from the key; enforced at read time via a superset-limit fallback in snapshot_get (stored limit >= requested serves, truncated, served_from_superset=true; a smaller stored limit still MISSES). snapshot_put records the stored limit.
  3. Empty-payload poisoning. list_issues(state="open") returns [] (invalid Linear state) and the hook cached it. Now snapshot_get returns a miss for an auto_populated payload with issues==[], and the auto-populate hook skips the write when the request state was an alias/invalid AND the result is empty. A manual empty put for a genuinely-empty slice still hits.

Tests

  • +7 behavioral tests: canonicalization hit, superset truncation, smaller-can't-serve-larger, exact-limit-not-flagged, closed-bucket isolation, poisoning guard, manual-empty-still-hits.
  • Updated the 6 assertions that encoded the pre-fix multi-key / limit-sensitive behavior to the new convergence contract (not suppressed — inverted with TAP-4588 rationale). The TAP-1374 cross-alias unlock contract is preserved by convergence (verified by TestAliasUnlock).
  • Full tapps-mcp unit suite: 5952 passed, 22 skipped, 0 failed. ruff + mypy clean on changed files (two pre-existing RUF003 ×-in-docstring warnings on master left untouched).
  • Live e2e verified: hook writes under state="started" → server get(state="open") HITS; list_issues(state="open")→[] does not poison the good snapshot.

🤖 Generated with Claude Code

The Linear snapshot payload cache almost never self-hit: the reader
(tapps_linear_snapshot_get) and the writer (auto-populate hook + agents'
own get/put) computed different keys for the same open-issue slice. The
sentinel/gate layer already collapsed aliases (TAP-1374) but the payload
key did not.

Root cause + fix:
- state alias: add _canonical_state() mapping ""/None/"open"/open-bucket
  members to one canonical "open" token; _resolve_cache_key canonicalizes
  the filename segment AND the hashed state field. Closed buckets
  (completed/canceled) stay isolated. Mirrored in the two bash key
  snippets, the PowerShell variant, and all 3 regenerated committed hooks.
- limit in hash: dropped from the key entirely; enforced at read time via
  a superset-limit fallback in snapshot_get (stored>=requested serves,
  truncated, served_from_superset=true; smaller stored limit MISSES).
  snapshot_put now records the stored limit.
- poisoning: snapshot_get returns a miss (not a false empty hit) for an
  auto_populated payload with issues==[]; the auto-populate hook skips the
  write when the request state was an alias/invalid AND the result is empty.

Tests: +7 behavioral tests (canonicalization hit, superset truncation,
smaller-can't-serve-larger, closed-bucket isolation, poisoning guard,
manual-empty-still-hits). Updated the assertions that encoded the pre-fix
multi-key/limit-sensitive behavior to the new convergence contract
(TAP-1374 cross-alias unlock preserved via convergence). Full tapps-mcp
unit suite green (5952 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wtthornton wtthornton merged commit a5a0e6d into master Jul 2, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89dfc85abe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +139 to +140
if state_lc == "" or state_lc == _CANONICAL_OPEN_STATE or state_lc in _OPEN_STATE_BUCKETS:
return _CANONICAL_OPEN_STATE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not collapse concrete Linear states into one cache key

When a cache entry is populated from a concrete list_issues call such as state="started", this canonicalization makes snapshot_get(state="backlog"), snapshot_get(state="unstarted"), and snapshot_get(state="open") all read the same file. The existing linear-read flow explicitly fetches and stores using the same concrete state as the requested list_issues filter (.claude/skills/linear-read/SKILL.md lines 16-21), so a narrow started-only payload can be served as a backlog result without another Linear fetch, causing agents to miss or act on the wrong issues. Keep the broad open alias separate from concrete state-filtered snapshots, or only store under open when the payload actually contains the full open-state superset.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant